package core;

/**
 * A class that models the Control Unit of a the processor SEP16
 * @author Ximo Tur ximotur at gmail dot com
 *
 */
public class ControlUnitClass {
	private short PC = 0;
	private short prevPC = 0;
	private short IR = 0;
	private short IR2 = 0;
	private short InterruptRegister = 0;
	public boolean STOP = false;

	private core.AluClass alu = null;
	private core.RegisterClass rb = null;
	private core.MemoryClass mem = null;
	private core.PsrClass psr = null;

	private core.interfaces.InterruptInterface iInterface = null;

	/**
	 * Constructor of the ControlUnit
	 * @param alu is the Processor ALU
	 * @param rb is the Processor Register Bank
	 * @param mem is the Processor Memory Bank
	 * @param psr is the Processor Status Register
	 */
	public ControlUnitClass(core.AluClass alu, core.RegisterClass rb, core.MemoryClass mem, core.PsrClass psr){
		this.alu = alu;
		this.rb = rb;
		this.mem = mem;
		this.psr = psr;
	}

	public void setInterruptInterface(core.interfaces.InterruptInterface iObject){
		this.iInterface = iObject;
	}
	/**
	 * Fetches the next instruction from the Memory Bank and Processes it
	 */
	public void processNextInstruction(){
		if(!this.testToCallInterrupt()){
			short wordInstruction = this.mem.getWord(this.getPC());
			this.incrementPC();
			core.InstructionClass i = new core.InstructionClass(wordInstruction);
			this.IR = i.getByteCode();
			if(i.inst.getFormat() == 4){
				wordInstruction = this.mem.getWord(this.getPC());
				this.incrementPC2();
				i.setByteCode2(wordInstruction);
				this.IR2 = i.getByteCode2();
			}
			processInstruction(i);
		}
	}

	public void processNextInstructionVerbose(){
		if(!this.testToCallInterrupt()){
			System.out.println("**************************");
			System.out.printf("PC: 0x%04X\n", this.getPC());
			short wordInstruction = this.mem.getWord(this.getPC());
			this.incrementPC();
			core.InstructionClass i = new core.InstructionClass(wordInstruction);
			this.IR = i.getByteCode();
			if(i.inst.getFormat() == 4){
				wordInstruction = this.mem.getWord(this.getPC());
				this.incrementPC2();
				i.setByteCode2(wordInstruction);
				this.IR2 = i.getByteCode2();
				System.out.printf("Instruction: 0x%04X 0x%04X (%s)\n", this.IR, this.IR2, i.getAssembly());
				System.out.println("**************************");
			}else{
				this.IR2 = 0;
				System.out.printf("Instruction: 0x%04X (%s)\n", this.IR, i.getAssembly());
				System.out.println("**************************");
			}
			processInstruction(i);
		}
	}
	/**
	 * Sets an arbitrary value for the PC
	 * @param newPC the new PC value
	 */
	public void setPC(short newPC){
		//Used by C and J instr, so don't touch prevPC
		this.PC = newPC;
	}
	public short getPC(){
		return(this.PC);
	}
	public void incrementPC(){
		this.prevPC = this.PC;
		this.PC++;
	}
	//Used for 2-word instructions
	public void incrementPC2(){
		this.PC++;
	}
	public short getPrevPC(){
		return(this.prevPC);
	}

	/**
	 * Processes the instruction.
	 * @param i is the instruction to be processed
	 */
	private void processInstruction(core.InstructionClass i){
		short aux;
		switch(i.inst){
			case MOV:
				this.rb.setRegister(i.r_op1, this.rb.getRegister(i.r_op2));
				break;
			case IMOV:
				this.mem.setWord(this.rb.getRegister(i.r_op1), this.rb.getRegister(i.r_op2));
				break;
			case MOVI:
				this.rb.setRegister(i.r_op1, this.mem.getWord(this.rb.getRegister(i.r_op2)));
				break;
			case MMOV:
				this.mem.setWord(i.r_op2, this.rb.getRegister(i.r_op1));
				break;
			case MOVM:
				this.rb.setRegister(i.r_op1, this.mem.getWord(i.r_op2));
				break;
			case LDR:
				this.rb.setRegister(i.r_op1, i.r_op2);
				break;
			case SPB:
				aux = this.rb.getRegister(i.r_op2);
				this.rb.setRegister(i.r_op1, (short)((aux>>8) & 0x00FF));
				this.rb.setRegister(i.r_op2, (short)(aux & 0x00FF));
				break;
			case MLB:
				aux = (short)((this.rb.getRegister(i.r_op1) & 0xFF00) |  (this.rb.getRegister(i.r_op2) & 0x00FF));
				this.rb.setRegister(i.r_op1, aux);
				break;
			case MHB:
				aux = (short)((this.rb.getRegister(i.r_op1) & 0x00FF) |  ((this.rb.getRegister(i.r_op2) & 0x00FF)<<8));
				this.rb.setRegister(i.r_op1, aux);
				break;
			case ADD:
			case ADC:
			case SUB:
			case SBC:
			case AND:
			case OR:
			case XOR:
			case MVN:
			case BIC:
				this.alu.setOperands(this.rb.getRegister(i.r_op1), this.rb.getRegister(i.r_op2));
				this.alu.setOperation(i.inst.getAluOperation());
				this.alu.performOperation();
				this.rb.setRegister(i.r_res, this.alu.getResult());
				break;
			case CMP:
				this.alu.setOperands(this.rb.getRegister(i.r_op1), this.rb.getRegister(i.r_op2));
				this.alu.setOperation(i.inst.getAluOperation());
				this.alu.performOperation();
				break;
			case INC:
			case DEC:
				this.alu.setOperands(this.rb.getRegister(i.r_op1), (short)1);
				this.alu.setOperation(i.inst.getAluOperation());
				this.alu.performOperation();
				this.rb.setRegister(i.r_res, this.alu.getResult());
				break;
			case ADDI:
			case ADCI:
			case SUBI:
			case SBCI:
			case ANDI:
			case ORI:
			case XORI:
			case BICI:
			case SHL:
			case SHR:
				this.alu.setOperands(this.rb.getRegister(i.r_op1), i.r_op2);
				this.alu.setOperation(i.inst.getAluOperation());
				this.alu.performOperation();
				this.rb.setRegister(i.r_res, this.alu.getResult());
				break;				
			case CMPI:
				this.alu.setOperands(this.rb.getRegister(i.r_op1), i.r_op2);
				this.alu.setOperation(i.inst.getAluOperation());
				this.alu.performOperation();
				break;
			case RET:
				this.rb.increaseSP();
				this.setPC(this.mem.getWord(this.rb.getSP()));
				break;
			case PUSHSR:
				this.mem.setWord(this.rb.getSP(), this.psr.getFlags());
				this.rb.decreaseSP();
				break;
			case POPSR:
				this.rb.increaseSP();
				this.psr.setFlags(this.mem.getWord(this.rb.getSP()));
				break;
			case PUSH:
				this.mem.setWord(this.rb.getSP(), this.rb.getRegister(i.r_op1));
				this.rb.decreaseSP();
				break;
			case POP:
				this.rb.increaseSP();
				this.rb.setRegister(i.r_op1, this.mem.getWord(this.rb.getSP()));
				break;
			case J:
				if(this.conditionEval(i.cond)){
					this.setPC(i.r_op2);
				}
				break;
			case JR:
				if(this.conditionEval(i.cond)){
					this.setPC(this.rb.getRegister(i.r_op2));
				}
				break;
			case C:
				if(this.conditionEval(i.cond)){
					this.mem.setWord(this.rb.getSP(), this.getPC());
					this.rb.decreaseSP();
					this.setPC(i.r_op2);
				}
				break;
			case CR:
				if(this.conditionEval(i.cond)){
					this.mem.setWord(this.rb.getSP(), this.getPC());
					this.rb.decreaseSP();
					this.setPC(this.rb.getRegister(i.r_op2));
				}
				break;
			case EI:
				this.psr.setEI(1);
				break;
			case DI:
				this.psr.setEI(0);
				break;
			case MI:
				switch(i.r_op1){
					case 1:
						this.psr.setTIM(1);
						break;
					case 2:
						this.psr.setHIM(1);
						break;
					case 3:
						this.psr.setLIM(1);
						break;
				}
				break;
			case UMI:
				switch(i.r_op1){
					case 1:
						this.psr.setTIM(0);
						break;
					case 2:
						this.psr.setHIM(0);
						break;
					case 3:
						this.psr.setLIM(0);
						break;
				}
				break;
			case IRET:
				this.rb.increaseSP();
				this.psr.setFlags(this.mem.getWord(this.rb.getSP()));
				this.rb.increaseSP();
				this.setPC(this.mem.getWord(this.rb.getSP()));
				break;
			case SWI:
				this.callInterrupt(core.ControlUnitClass.Interrupts.SOFTWARE);
				//Alternative, let Software Interrupt to be sensitive to EI bit:
				//this.setInterruptBit(core.ControlUnitClass.Interrupts.SOFTWARE.getBit());
				break;
			case STOP:
				this.STOP = true;
				break;
			case NOP:
			default: //Default is a nop
				break;
		}
	}
	private boolean conditionEval(core.InstructionClass.ConditionalCodes c){
		switch(c){
			case AL:
				return(true);
			case EQ:
				return(this.psr.getZ() == 1);
			case NE:
				return(this.psr.getZ() == 0);
			case CS:
				return(this.psr.getC() == 1);
			case CC:
				return(this.psr.getC() == 0);
			case PL:
				return(this.psr.getN() == 0);
			case MI:
				return(this.psr.getN() == 1);
			case HI:
				return((this.psr.getZ() == 0) & (this.psr.getC() == 1));
			case LS:
				return((this.psr.getZ() == 1)|(this.psr.getC() == 0));
			case GE:
				return(this.psr.getN() == this.psr.getV());
			case GT:
				return((this.psr.getN() == this.psr.getV()) & (this.psr.getZ() == 0));
			case LE:
				return((this.psr.getN() != this.psr.getV()) | (this.psr.getZ() == 1));
			case LT:
				return(this.psr.getN() != this.psr.getV());
			default:
				return(false);
		}
	}

	/**
	 * Check if we have to serve an interruption and call the routine that effectively changes PC, stack...
	 */
	private boolean testToCallInterrupt(){
		boolean haveToCall = false;
		if((this.psr.getEI() == 1) && (this.InterruptRegister != 0)){
			haveToCall = true;
			//System.out.printf("Interrupts activated and pending interrupt exists (0x%04x)\n", this.InterruptRegister);
			if((this.InterruptRegister & 1<<core.ControlUnitClass.Interrupts.RESET.getBit()) != 0){
				//Reset Interrupt
				this.callInterrupt(core.ControlUnitClass.Interrupts.RESET);
			}else if((this.InterruptRegister & 1<<core.ControlUnitClass.Interrupts.TIMER.getBit()) != 0){
				//Timer Interrupt
				this.callInterrupt(core.ControlUnitClass.Interrupts.TIMER);
			}else if((this.InterruptRegister & 1<<core.ControlUnitClass.Interrupts.HIGH.getBit()) != 0){
				//High Interrupt
				this.callInterrupt(core.ControlUnitClass.Interrupts.HIGH);
			}else if((this.InterruptRegister & 1<<core.ControlUnitClass.Interrupts.LOW.getBit()) != 0){
				//Low Interrupt
				this.callInterrupt(core.ControlUnitClass.Interrupts.LOW);
			}else if((this.InterruptRegister & 1<<core.ControlUnitClass.Interrupts.SOFTWARE.getBit()) != 0){
				//Low Interrupt
				this.callInterrupt(core.ControlUnitClass.Interrupts.SOFTWARE);
			}
		}
		return(haveToCall);
	}
	/**
	 * Configure Processor to serve an interruption:
	 * 	- Save PC and PSR in the stack
	 *  - Disable Interrupts
	 *  - Call the Interrupt routine service
	 * @param i is the Interrupt type for which we have to configure the Processor
	 */
	protected void callInterrupt(core.ControlUnitClass.Interrupts i){
		util.Messages.info("CONTROL: Routine to call " + i.name() + " interrupt");
		this.mem.setWord(this.rb.getSP(), this.getPC());
		this.rb.decreaseSP();
		this.mem.setWord(this.rb.getSP(), this.psr.getFlags());
		this.rb.decreaseSP();
		this.psr.setEI(0);
		this.setPC(i.getServiceAddress());
		//Possibly done by a separate IACK instruction:
		this.clearInterruptBit(i.getBit());
		if(this.iInterface != null){
			this.iInterface.lowerInterruptLine(i.getBit());
		}
	}
	synchronized protected void clearInterruptBit(int bit){
		//System.out.printf("****************************\nCLEAR bit %d\nInterrupt Register: 0x%04x\n", bit, this.InterruptRegister);
		this.InterruptRegister = (short)(this.InterruptRegister & ~(1<<(bit)));
		//System.out.printf("New Iterrupt Register: 0x%04x\n", this.InterruptRegister);
	}
	synchronized protected void setInterruptBit(int bit){
		//System.out.printf("****************************\nSET bit %d\nInterrupt Register: 0x%04x\n", bit, this.InterruptRegister);
		this.InterruptRegister = (short)(this.InterruptRegister | (1<<(bit)));
		//System.out.printf("New Iterrupt Register: 0x%04x\n", this.InterruptRegister);
	}

	protected enum Interrupts{
		RESET((short)0x0000, 0),
		TIMER((short)0x0002, 1),
		HIGH((short)0x0004, 2),
		LOW((short)0x0006, 3),
		SOFTWARE((short)0x0008, 4);

		private short addr;
		private int bit;

		Interrupts(short address, int bit){
			this.addr = address;
			this.bit = bit;
		}
		public short getServiceAddress(){
			return(this.addr);
		}
		public int getBit(){
			return(this.bit);
		}
	}
}
